Skip to content

Commit bfa044b

Browse files
thoniTUBawildturtok
andcommitted
fix: filter value resolving chunk solr request by uri length (#3899)
Co-authored-by: awildturtok <1553491+awildturtok@users.noreply.github.com>
1 parent e3a3933 commit bfa044b

2 files changed

Lines changed: 308 additions & 43 deletions

File tree

backend/src/main/java/com/bakdata/conquery/util/search/solr/FilterValueSearch.java

Lines changed: 88 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,5 @@
11
package com.bakdata.conquery.util.search.solr;
22

3-
import java.io.IOException;
4-
import java.net.URLDecoder;
5-
import java.nio.charset.StandardCharsets;
6-
import java.util.ArrayList;
7-
import java.util.Arrays;
8-
import java.util.Collection;
9-
import java.util.List;
10-
import java.util.Map;
11-
import java.util.concurrent.atomic.AtomicLong;
12-
import java.util.function.Function;
13-
import java.util.function.Predicate;
14-
import java.util.stream.Collectors;
15-
import java.util.stream.Stream;
16-
import javax.annotation.CheckForNull;
17-
import javax.annotation.Nullable;
18-
193
import com.bakdata.conquery.apiv1.frontend.FrontendValue;
204
import com.bakdata.conquery.models.config.search.solr.FilterValueConfig;
215
import com.bakdata.conquery.models.datasets.concepts.Searchable;
@@ -24,7 +8,6 @@
248
import com.bakdata.conquery.resources.api.ConceptsProcessor;
259
import com.bakdata.conquery.resources.api.ConceptsProcessor.AutoCompleteResult;
2610
import com.bakdata.conquery.util.search.solr.entities.SolrFrontendValue;
27-
import com.google.common.collect.Iterables;
2811
import lombok.AllArgsConstructor;
2912
import lombok.extern.slf4j.Slf4j;
3013
import org.apache.commons.lang3.StringUtils;
@@ -38,6 +21,18 @@
3821
import org.apache.solr.common.SolrDocument;
3922
import org.jetbrains.annotations.NotNull;
4023

24+
import javax.annotation.CheckForNull;
25+
import javax.annotation.Nullable;
26+
import java.io.IOException;
27+
import java.net.URLDecoder;
28+
import java.nio.charset.StandardCharsets;
29+
import java.util.*;
30+
import java.util.concurrent.atomic.AtomicLong;
31+
import java.util.function.Function;
32+
import java.util.function.Predicate;
33+
import java.util.stream.Collectors;
34+
import java.util.stream.Stream;
35+
4136
/**
4237
* Helper class to abstract/capsule the {@link Searchable}s of the filter away.
4338
* The data was/is imported to Solr by a {@link FilterValueIndexer} for each {@link Searchable}, because a single {@link Searchable} might be used by multiple {@link Filter}s.
@@ -46,7 +41,7 @@
4641
@Slf4j
4742
public class FilterValueSearch {
4843

49-
public static final int EXACT_VALUE_CHUNK_SIZE = 200;
44+
public static final int SOLR_MAX_URI_LENGTH = 5000 /* bytes */ ; // with some buffer, actual limit is 8192, but setting this to 6000 already causes failures
5045
private final SelectFilter<?> filter;
5146
private final SolrProcessor processor;
5247
private final SolrClient solrClient;
@@ -186,50 +181,41 @@ public ConceptsProcessor.ExactFilterValueResult exact(Collection<String> terms)
186181
final Map<String, String> unresolvedMap = terms.stream().collect(Collectors.toMap(String::toLowerCase, Function.identity(), (v1, v2) -> v1));
187182
final List<FrontendValue> resolved = new ArrayList<>(terms.size());
188183

189-
/*
190-
We chunk the values for resolving here so that the request does not bust any URI or query limitations (e.g. "too many boolean operators")
191-
*/
192-
int chunkIndex = 1;
193-
int chunkCount = (escapedTerms.size() + (EXACT_VALUE_CHUNK_SIZE - 1)) / EXACT_VALUE_CHUNK_SIZE;
194-
final Iterable<List<String>> partition = Iterables.partition(escapedTerms, EXACT_VALUE_CHUNK_SIZE);
195-
for (List<String> chunk : partition) {
196184

197-
String finalTerms = chunk.stream().collect(Collectors.joining(" ", "(", ")"));
185+
List<List<String>> chunks = chunkByUriLength(escapedTerms);
186+
int chunkIndex = 1;
187+
final int chunkCount = chunks.size();
188+
int source_count = getSearchesFor(filter, false).size();
189+
for (List<String> chunk : chunks) {
190+
191+
String queryString = buildExactQuery(chunk);
198192

199-
if (StringUtils.isBlank(finalTerms)) {
193+
if (StringUtils.isBlank(queryString)) {
200194
return new ConceptsProcessor.ExactFilterValueResult(List.of(), terms);
201195
}
202196

203-
// We are matching on label and value.
204-
// So if for reason a value is present in multiple sources (map, template, ...) but has different labels
205-
// both can be found.
206-
String collect = Stream.of(
207-
SolrFrontendValue.Fields.value_s,
208-
SolrFrontendValue.Fields.label_t
209-
)
210-
.map(field -> "%s:%s".formatted(field, finalTerms))
211-
// We are not interested in the result score, so we make it static: ^=1
212-
.collect(Collectors.joining(" OR ", "(", ")^=1"));
213-
214-
215197
// The batchsize is twice the size of the chunk size because a term is often (at most) found in two documents (from the column and from a mapping)
216-
// Technically a filter could use more than 2 sources, but this is not practical
217-
final int batchSize = EXACT_VALUE_CHUNK_SIZE * 2;
198+
final int batchSize = chunk.size() * source_count;
218199

219200
final AtomicLong numFound = new AtomicLong();
220201
try {
221202
List<FrontendValue> resolvedValues = new ArrayList<>();
222203

223204
// We sort to return value with the highest source priority and get the best description
224-
SolrQuery solrQuery = buildSolrQuery(collect, 0, batchSize, true, false, false);
205+
SolrQuery solrQuery = buildSolrQuery(queryString, 0, batchSize, true, false, false);
206+
225207

226208
String decodedQuery = URLDecoder.decode(String.valueOf(solrQuery), StandardCharsets.UTF_8);
227209
int queryHash = decodedQuery.hashCode();
228210
log.trace("Query [{}] ({}/{}) created: {}", queryHash, chunkIndex, chunkCount, decodedQuery);
229211

212+
int queryByteLength = solrQuery.toString().getBytes(StandardCharsets.UTF_8).length;
213+
log.trace("Query [{}] length in bytes: {}", queryHash, queryByteLength);
214+
230215
QueryResponse response = solrClient.queryAndStreamResponse(solrQuery, new StreamingResponseCallback() {
231216
@Override
232217
public void streamSolrDocument(SolrDocument doc) {
218+
log.trace("Query [{}] received document: {}", queryHash, doc);
233219
if (unresolvedMap.isEmpty()) {
234220
// Shortcut: everything was resolved
235221
return;
@@ -252,7 +238,7 @@ public void streamSolrDocument(SolrDocument doc) {
252238
public void streamDocListInfo(long numFoundCallBack, long start, Float maxScore) {
253239
numFound.set(numFoundCallBack);
254240
if (numFoundCallBack > batchSize) {
255-
log.warn("Query found more documents ({}) than expected ({}). We expect a term to be found in at most 2 documents (from a column and a mapping).", numFoundCallBack, batchSize);
241+
log.warn("Query found more documents ({}) than expected ({}). We expect a term to be found in at most {} documents (from a column and a mapping).", numFoundCallBack, batchSize, source_count);
256242
}
257243
}
258244
});
@@ -269,4 +255,63 @@ public void streamDocListInfo(long numFoundCallBack, long start, Float maxScore)
269255
return new ConceptsProcessor.ExactFilterValueResult(resolved, unresolvedMap.values());
270256
}
271257

258+
private List<List<String>> chunkByUriLength(Collection<String> terms) {
259+
List<List<String>> result = new ArrayList<>();
260+
List<String> current = new ArrayList<>();
261+
262+
for (String term : terms) {
263+
if (StringUtils.isBlank(term)) {
264+
continue;
265+
}
266+
current.add(term);
267+
268+
String query = buildExactQuery(current);
269+
270+
int size = query.getBytes(StandardCharsets.UTF_8).length;
271+
272+
if (size <= SOLR_MAX_URI_LENGTH) {
273+
// Length still fine, let's move on
274+
continue;
275+
}
276+
277+
// remove last term and finish current batch
278+
current.removeLast();
279+
280+
if (current.isEmpty()) {
281+
// single term too large for query
282+
throw new IllegalArgumentException("Single term is too large for URI. Term: %s".formatted(term));
283+
} else {
284+
result.add(current);
285+
current = new ArrayList<>();
286+
current.add(term);
287+
}
288+
}
289+
290+
if (!current.isEmpty()) {
291+
result.add(current);
292+
}
293+
294+
return result;
295+
}
296+
297+
@NotNull
298+
private static String buildExactQuery(List<String> chunk) {
299+
String finalTerms = chunk.stream().collect(Collectors.joining(" ", "(", ")"));
300+
301+
if (StringUtils.isBlank(finalTerms)) {
302+
return finalTerms;
303+
}
304+
305+
// We are matching on label and value.
306+
// So if for reason a value is present in multiple sources (map, template, ...) but has different labels
307+
// both can be found.
308+
return Stream.of(
309+
SolrFrontendValue.Fields.value_s,
310+
SolrFrontendValue.Fields.label_t
311+
)
312+
.map(field -> "%s:%s".formatted(field, finalTerms))
313+
// We are not interested in the result score, so we make it static: ^=1
314+
.collect(Collectors.joining(" OR ", "(", ")^=1"));
315+
}
316+
272317
}

0 commit comments

Comments
 (0)