Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support index pattern #316

Merged
merged 11 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion docs/overview/requests/elastic_request_get_by_id.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import zio.elasticsearch.ElasticRequest.getById
// this import is required for using `IndexName` and `DocumentId`
import zio.elasticsearch._

val request: ExistsRequest = getById(index = IndexName("index"), id = DocumentId("111"))
val request: GetByIdRequest = getById(index = IndexName("index"), id = DocumentId("111"))
```

If you want to change the `refresh`, you can use `refresh`, `refreshFalse` or `refreshTrue` method:
Expand All @@ -30,4 +30,9 @@ import zio.elasticsearch._
val requestWithRouting: GetByIdRequest = getById(index = IndexName("index"), id = DocumentId("111")).routing(Routing("routing"))
```

If you want to create `GetById` request with `IndexPattern`, you can do that in the following manner:
```scala
val requestWithIndexPattern: GetByIdRequest = getById(index = IndexPattern("index*"), id = DocumentId("111"))
```

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe put this after this part To create a GetById request do the following:.

You can find more information about `GetById` request [here](https://www.elastic.co/guide/en/elasticsearch/reference/7.17/docs-get.html).
10 changes: 10 additions & 0 deletions docs/overview/requests/elastic_request_refresh.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,15 @@ import zio.elasticsearch._
val request: RefreshRequest = refresh(index = IndexName("index"))
```

If you want to refresh more indices, you can use `refresh` method this way:
```scala
val request: RefreshRequest = refresh(index = MultiIndex.names(IndexName("index1"), IndexName("index2")))
```

If you want to refresh all indices, you can use `refresh` method with `IndexPattern` this way:
```scala
val request: RefreshRequest = refresh(index = IndexPattern("_all"))
```

You can find more information about `Refresh` request [here](https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-refresh.html).

5 changes: 5 additions & 0 deletions docs/overview/requests/elastic_request_search.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,9 @@ val request1WithSort: SearchRequest = search(index = IndexName("index"), query =
val request2WithSort: SearchAndAggregateRequest = search(index = IndexName("index"), query = matchAll, aggregation = maxAggregation(name = "aggregation", field = "intField")).sort(sortBy("intField").missing(First))
```

If you want to create `Search` request with `IndexPattern`, you can do that in the following manner:
```scala
val requestWithIndexPattern: SearchRequest = search(index = IndexPattern("index*"), query = matchAll)
```

You can find more information about `Search` and `SearchAndAggregate` requests [here](https://www.elastic.co/guide/en/elasticsearch/reference/7.17/search-search.html).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add new line at the end of the file?

Original file line number Diff line number Diff line change
Expand Up @@ -686,16 +686,23 @@ object HttpExecutorSpec extends IntegrationSpec {
),
suite("refresh index")(
test("successfully refresh existing index") {
assertZIO(Executor.execute(ElasticRequest.refresh(index)))(
equalTo(true)
)
assertZIO(Executor.execute(ElasticRequest.refresh(index)))(equalTo(true))
},
test("successfully refresh more existing indices") {
for {
_ <- Executor.execute(ElasticRequest.createIndex(createIndexTestName))
res <- Executor.execute(ElasticRequest.refresh(MultiIndex.names(index, createIndexTestName)))
} yield assert(res)(equalTo(true))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's replace all equalTo(true) with isTrue.

},
test("successfully refresh all indices") {
assertZIO(Executor.execute(ElasticRequest.refresh(IndexPatternAll)))(equalTo(true))
},
test("return false if index does not exists") {
assertZIO(Executor.execute(ElasticRequest.refresh(refreshFailIndex)))(
equalTo(false)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess you can use isFalse here.

)
}
),
) @@ after(Executor.execute(ElasticRequest.deleteIndex(createIndexTestName)).orDie),
suite("retrieving document by IDs")(
test("find documents by ids") {
checkOnce(genDocumentId, genTestDocument, genDocumentId, genTestDocument) {
Expand Down Expand Up @@ -979,6 +986,36 @@ object HttpExecutorSpec extends IntegrationSpec {
Executor.execute(ElasticRequest.createIndex(firstSearchIndex)),
Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie
),
test("search for a document using a match all query with index pattern") {
checkOnce(genDocumentId, genTestDocument, genDocumentId, genTestDocument) {
(firstDocumentId, firstDocument, secondDocumentId, secondDocument) =>
for {
_ <- Executor.execute(ElasticRequest.deleteByQuery(firstSearchIndex, matchAll))
_ <- Executor.execute(ElasticRequest.deleteByQuery(secondSearchIndex, matchAll))
document = firstDocument.copy(stringField = s"this is test")
_ <-
Executor.execute(
ElasticRequest.upsert[TestDocument](firstSearchIndex, firstDocumentId, document).refreshTrue
)
secondDocumentCopy = secondDocument.copy(stringField = s"this is test")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
secondDocumentCopy = secondDocument.copy(stringField = s"this is test")
secondDocumentCopy = secondDocument.copy(stringField = "this is test")

_ <- Executor.execute(
ElasticRequest
.upsert[TestDocument](secondSearchIndex, secondDocumentId, secondDocumentCopy)
.refreshTrue
)
query = matchAll
res <- Executor
.execute(ElasticRequest.search(IndexPattern("search-index*"), query))
.documentAs[TestDocument]
} yield assert(res)(Assertion.contains(document)) && assert(res)(Assertion.contains(secondDocumentCopy))
}
} @@ around(
Executor.execute(ElasticRequest.createIndex(firstSearchIndex)),
Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie
) @@ around(
Executor.execute(ElasticRequest.createIndex(secondSearchIndex)),
Executor.execute(ElasticRequest.deleteIndex(secondSearchIndex)).orDie
),
test("search for a document using a match boolean prefix query") {
checkOnce(genDocumentId, genTestDocument, genDocumentId, genTestDocument) {
(firstDocumentId, firstDocument, secondDocumentId, secondDocument) =>
Expand All @@ -1000,6 +1037,40 @@ object HttpExecutorSpec extends IntegrationSpec {
Executor.execute(ElasticRequest.createIndex(firstSearchIndex)),
Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie
),
test("search for a document using a match phrase query with multi index") {
checkOnce(genDocumentId, genTestDocument, genDocumentId, genTestDocument) {
(firstDocumentId, firstDocument, secondDocumentId, secondDocument) =>
for {
_ <- Executor.execute(ElasticRequest.deleteByQuery(firstSearchIndex, matchAll))
_ <- Executor.execute(ElasticRequest.deleteByQuery(secondSearchIndex, matchAll))
document = firstDocument.copy(stringField = s"this is test")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you named copy of secondDocument: secondDocumentCopy, you should call this one firstDocumentCopy.

_ <-
Executor.execute(
ElasticRequest.upsert[TestDocument](firstSearchIndex, firstDocumentId, document).refreshTrue
)
secondDocumentCopy = secondDocument.copy(stringField = s"this is test")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
secondDocumentCopy = secondDocument.copy(stringField = s"this is test")
secondDocumentCopy = secondDocument.copy(stringField = "this is test")

_ <- Executor.execute(
ElasticRequest
.upsert[TestDocument](secondSearchIndex, secondDocumentId, secondDocumentCopy)
.refreshTrue
)
query = matchPhrase(
field = TestDocument.stringField,
value = document.stringField
)

res <- Executor
.execute(ElasticRequest.search(MultiIndex.names(firstSearchIndex, secondSearchIndex), query))
.documentAs[TestDocument]
} yield assert(res)(Assertion.contains(document)) && assert(res)(Assertion.contains(secondDocumentCopy))
}
} @@ around(
Executor.execute(ElasticRequest.createIndex(firstSearchIndex)),
Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie
) @@ around(
Executor.execute(ElasticRequest.createIndex(secondSearchIndex)),
Executor.execute(ElasticRequest.deleteIndex(secondSearchIndex)).orDie
),
test("search for a document using a match phrase query") {
checkOnce(genDocumentId, genTestDocument, genDocumentId, genTestDocument) {
(firstDocumentId, firstDocument, secondDocumentId, secondDocument) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ trait IntegrationSpec extends ZIOSpecDefault {

val refreshFailIndex: IndexName = IndexName("refresh-fail")

val IndexPatternAll: IndexPattern = IndexPattern("_all")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use a constant?


val prepareElasticsearchIndexForTests: TestAspect[Nothing, Any, Throwable, Any] = beforeAll((for {
_ <- Executor.execute(ElasticRequest.createIndex(index))
_ <- Executor.execute(ElasticRequest.deleteByQuery(index, matchAll).refreshTrue)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2022 LambdaWorks
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package zio.elasticsearch

import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.StringUtils.{equalsAny, startsWithAny}
import zio.Chunk

object IndexPatternValidation {
def isValid(pattern: String): Boolean = {
def containsAny(string: String, params: Chunk[String]): Boolean =
params.exists(StringUtils.contains(string, _))

pattern.toLowerCase == pattern &&
pattern.nonEmpty &&
!startsWithAny(pattern, "+") &&
!containsAny(string = pattern, params = Chunk("?", "\"", "<", ">", "|", " ", ",", "#", ":")) &&
!equalsAny(pattern, ".", "..") &&
pattern.getBytes().length <= 255
}
}
113 changes: 113 additions & 0 deletions modules/library/src/main/scala-2/zio/elasticsearch/MultiIndex.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright 2022 LambdaWorks
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package zio.elasticsearch

import zio.Chunk
import zio.prelude.AssertionError.failure
import zio.prelude.Newtype

trait IndexSelector[A] {
def toSelector(a: A): String
}

object IndexSelector {

implicit object IndexNameSelector extends IndexSelector[IndexName] {
def toSelector(name: IndexName): String = IndexName.unwrap(name)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put implementation in the new line.

}

implicit object IndexPatternSelector extends IndexSelector[IndexPattern] {
def toSelector(pattern: IndexPattern): String = IndexPattern.unwrap(pattern)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

}

implicit object MultiIndexSelector extends IndexSelector[MultiIndex] {
def toSelector(multi: MultiIndex): String = multi.indices.mkString(",")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

}

implicit class IndexNameSyntax[A](a: A)(implicit IS: IndexSelector[A]) {
def toSelector: String = IS.toSelector(a)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

}

}

trait IndexNameNewtype {
object IndexName extends Newtype[String] {
override def assertion = assertCustom { (name: String) => // scalafix:ok
if (IndexNameValidation.isValid(name)) {
Right(())
} else {
Left(
failure(
s"""
| - Must be lower case only
| - Cannot include \\, /, *, ?, ", <, >, |, ` `(space character), `,`(comma), #.
| - Cannot include ":"(since 7.0).
| - Cannot be empty
| - Cannot start with -, _, +.
| - Cannot be `.` or `..`.
| - Cannot be longer than 255 bytes (note it is bytes, so multi-byte characters will count towards the 255 limit faster).
| - Names starting with . are deprecated, except for hidden indices and internal indices managed by plugins.
|""".stripMargin
)
)
}
}
}
type IndexName = IndexName.Type
}

trait IndexPatternNewType {
object IndexPattern extends Newtype[String] {
override def assertion = assertCustom { (pattern: String) => // scalafix:ok
if (IndexPatternValidation.isValid(pattern)) {
Right(())
} else {
Left(
failure(
s"""
| - Must be lower case only
| - Cannot include \\, /, ?, ", <, >, |, ` `(space character), `,`(comma), #.
| - Cannot include ":"(since 7.0).
| - Cannot be empty
| - Cannot start with +.
| - Cannot be `.` or `..`.
| - Cannot be longer than 255 bytes (note it is bytes, so multi-byte characters will count towards the 255 limit faster).
| - Patterns starting with . are deprecated, except for hidden indices and internal indices managed by plugins.
|""".stripMargin
)
)
}
}
}
type IndexPattern = IndexPattern.Type
}

final case class MultiIndex private (indices: Chunk[String]) { self =>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense to make this MultiIndex private[elasticsearch]. What do you think @drmarjanovic ?

def names(name: IndexName, names: IndexName*): MultiIndex =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put empty line between.

self.copy(indices = indices ++ Chunk.fromIterable(name.toString +: names.map(IndexName.unwrap)))

def patterns(pattern: IndexPattern, patterns: IndexPattern*): MultiIndex =
self.copy(indices = indices ++ Chunk.fromIterable(pattern.toString +: patterns.map(IndexPattern.unwrap)))
}

object MultiIndex {
def names(name: IndexName, names: IndexName*): MultiIndex =
new MultiIndex(Chunk.fromIterable(name.toString +: names.map(IndexName.unwrap)))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can omit new, as this is final case class.


def patterns(pattern: IndexPattern, patterns: IndexPattern*): MultiIndex =
new MultiIndex(Chunk.fromIterable(pattern.toString +: patterns.map(IndexPattern.unwrap)))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can omit new, as this is final case class.