Skip to content

Commit 6355a05

Browse files
authored
Merge pull request #112 from algolia/feature/relevance-helper-function
Relevance helper functions
2 parents 5911929 + 57b201d commit 6355a05

File tree

6 files changed

+94
-9
lines changed

6 files changed

+94
-9
lines changed

src/commonMain/kotlin/com/algolia/search/endpoint/EndpointSearch.kt

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ package com.algolia.search.endpoint
22

33
import com.algolia.search.model.Attribute
44
import com.algolia.search.model.IndexName
5-
import com.algolia.search.model.filter.Filter
65
import com.algolia.search.model.filter.FilterGroup
6+
import com.algolia.search.model.response.ResponseHitWithPosition
77
import com.algolia.search.model.response.ResponseSearch
88
import com.algolia.search.model.response.ResponseSearchForFacets
99
import com.algolia.search.model.search.Cursor
@@ -121,4 +121,26 @@ public interface EndpointSearch {
121121
filterGroups: Set<FilterGroup<*>> = setOf(),
122122
requestOptions: RequestOptions? = null
123123
): ResponseSearch
124-
}
124+
125+
/**
126+
* Search iteratively through the search response [ResponseSearch.hits] field to find the first response hit that
127+
* would match against the given [match] function.
128+
* If no object has been found within the first result set, the function
129+
* will perform a new search operation on the next page of results, if any,
130+
* until a matching object is found or the end of results is reached, whichever
131+
* happens first.
132+
* [doNotPaginate] will stop the function at the end of the first page of search results even if no object does
133+
* match.
134+
*
135+
* @param match Predicate to match a given [ResponseSearch.Hit]
136+
* @param query The [Query] used to search.
137+
* @param doNotPaginate To prevent the iteration through pages of results.
138+
* @param requestOptions Configure request locally with [RequestOptions].
139+
*/
140+
tailrec suspend fun findFirstObject(
141+
match: (ResponseSearch.Hit) -> Boolean,
142+
query: Query = Query(),
143+
doNotPaginate: Boolean = false,
144+
requestOptions: RequestOptions? = null
145+
): ResponseHitWithPosition?
146+
}

src/commonMain/kotlin/com/algolia/search/endpoint/EndpointSearchImpl.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.algolia.search.model.filter.Filter
99
import com.algolia.search.model.filter.FilterGroup
1010
import com.algolia.search.model.multipleindex.IndexQuery
1111
import com.algolia.search.model.request.RequestParams
12+
import com.algolia.search.model.response.ResponseHitWithPosition
1213
import com.algolia.search.model.response.ResponseSearch
1314
import com.algolia.search.model.response.ResponseSearchForFacets
1415
import com.algolia.search.model.response.ResponseSearches
@@ -34,6 +35,23 @@ internal class EndpointSearchImpl(
3435
return transport.request(HttpMethod.Post, CallType.Read, indexName.toPath("/query"), requestOptions, body)
3536
}
3637

38+
override tailrec suspend fun findFirstObject(
39+
match: (ResponseSearch.Hit) -> Boolean,
40+
query: Query,
41+
doNotPaginate: Boolean,
42+
requestOptions: RequestOptions?
43+
): ResponseHitWithPosition? {
44+
val response = search(query, requestOptions)
45+
val hit = response.hits.find(match)
46+
val hasNextPage = response.page + 1 < response.nbPages
47+
48+
return if (hit != null) {
49+
ResponseHitWithPosition(hit, response.hits.indexOf(hit), response.page)
50+
} else if (!doNotPaginate && hasNextPage) {
51+
findFirstObject(match, query.copy(page = (query.page ?: 0) + 1), doNotPaginate, requestOptions)
52+
} else null
53+
}
54+
3755
override suspend fun browse(query: Query, requestOptions: RequestOptions?): ResponseSearch {
3856
val params = RequestParams(query.toJsonNoDefaults().urlEncode())
3957
val body = JsonNoDefaults.stringify(RequestParams.serializer(), params)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.algolia.search.model.response
2+
3+
import kotlinx.serialization.Serializable
4+
5+
6+
@Serializable
7+
public data class ResponseHitWithPosition(
8+
val hit: ResponseSearch.Hit,
9+
val position: Int,
10+
val page: Int
11+
)

src/commonMain/kotlin/com/algolia/search/model/response/ResponseSearch.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.algolia.search.model.response
33
import com.algolia.search.endpoint.EndpointSearch
44
import com.algolia.search.model.Attribute
55
import com.algolia.search.model.IndexName
6+
import com.algolia.search.model.ObjectID
67
import com.algolia.search.model.QueryID
78
import com.algolia.search.model.insights.InsightsEvent
89
import com.algolia.search.model.search.*
@@ -272,6 +273,14 @@ public data class ResponseSearch(
272273
public val hierarchicalFacets: Map<Attribute, List<Facet>>
273274
get() = hierarchicalFacetsOrNull!!
274275

276+
/**
277+
* Returns the position (0-based) within the [hits] result list of the record matching against the given [objectID].
278+
* If the [objectID] is not found, -1 is returned.
279+
*/
280+
public fun getObjectIDPosition(objectID: ObjectID): Int {
281+
return hits.indexOfFirst { it.json.getPrimitiveOrNull("objectID")?.content == objectID.raw }
282+
}
283+
275284
/**
276285
* A Hit returned by the search.
277286
*/

src/commonTest/kotlin/suite/TestSuiteSearch.kt

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package suite
33
import clientAdmin1
44
import clientSearch
55
import com.algolia.search.helper.toAttribute
6+
import com.algolia.search.model.ObjectID
7+
import com.algolia.search.model.response.ResponseSearch
68
import com.algolia.search.model.search.Query
79
import com.algolia.search.model.settings.AttributeForFaceting
810
import com.algolia.search.model.settings.Settings
@@ -11,6 +13,7 @@ import com.algolia.search.model.task.TaskStatus
1113
import kotlinx.serialization.json.JsonObjectSerializer
1214
import kotlinx.serialization.list
1315
import runBlocking
16+
import shouldBeNull
1417
import shouldBeTrue
1518
import shouldContain
1619
import shouldEqual
@@ -46,13 +49,33 @@ internal class TestSuiteSearch {
4649
index.apply {
4750
tasks += setSettings(settings)
4851
tasks += saveObjects(objects)
49-
5052
tasks.wait().all { it is TaskStatus.Published }.shouldBeTrue()
51-
search(Query("algolia")).nbHits shouldEqual 2
52-
val hits = search(Query("elon", clickAnalytics = true)).hits
5353

54-
hits.shouldNotBeNull()
55-
hits.shouldNotBeEmpty()
54+
search(Query("algolia")).apply {
55+
nbHits shouldEqual 2
56+
getObjectIDPosition(ObjectID("nicolas-dessaigne")) shouldEqual 0
57+
getObjectIDPosition(ObjectID("julien-lemoine")) shouldEqual 1
58+
getObjectIDPosition(ObjectID("unknown")) shouldEqual -1
59+
}
60+
61+
findFirstObject({ false }, Query(""), false).shouldBeNull()
62+
findFirstObject({ true }, Query(""), false)!!.apply {
63+
page shouldEqual 0
64+
position shouldEqual 0
65+
}
66+
val predicate = { hit: ResponseSearch.Hit -> hit.json.getPrimitive("company").content == "Apple" }
67+
68+
findFirstObject(predicate, Query("Algolia"), false).shouldBeNull()
69+
findFirstObject(predicate, Query(hitsPerPage = 5), true).shouldBeNull()
70+
findFirstObject(predicate, Query(hitsPerPage = 5), false)!!.apply {
71+
position shouldEqual 0
72+
page shouldEqual 2
73+
}
74+
75+
search(Query("elon", clickAnalytics = true)).apply {
76+
hits.shouldNotBeNull()
77+
hits.shouldNotBeEmpty()
78+
}
5679
}
5780
search.apply {
5881
search(Query(facets = allFacets, filters = "company:tesla")).nbHits shouldEqual 1

src/commonTest/resources/companies.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
[
22
{
33
"company": "Algolia",
4-
"name": "Julien Lemoine"
4+
"name": "Julien Lemoine",
5+
"objectID": "julien-lemoine"
56
},
67
{
78
"company": "Algolia",
8-
"name": "Nicolas Dessaigne"
9+
"name": "Nicolas Dessaigne",
10+
"objectID": "nicolas-dessaigne"
911
},
1012
{
1113
"company": "Amazon",

0 commit comments

Comments
 (0)