11package 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-
193import com .bakdata .conquery .apiv1 .frontend .FrontendValue ;
204import com .bakdata .conquery .models .config .search .solr .FilterValueConfig ;
215import com .bakdata .conquery .models .datasets .concepts .Searchable ;
248import com .bakdata .conquery .resources .api .ConceptsProcessor ;
259import com .bakdata .conquery .resources .api .ConceptsProcessor .AutoCompleteResult ;
2610import com .bakdata .conquery .util .search .solr .entities .SolrFrontendValue ;
27- import com .google .common .collect .Iterables ;
2811import lombok .AllArgsConstructor ;
2912import lombok .extern .slf4j .Slf4j ;
3013import org .apache .commons .lang3 .StringUtils ;
3821import org .apache .solr .common .SolrDocument ;
3922import 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.
4641@ Slf4j
4742public 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