2424
2525import java .io .IOException ;
2626import java .nio .ByteBuffer ;
27+ import java .util .ArrayList ;
28+ import java .util .Collection ;
29+ import java .util .Collections ;
30+ import java .util .List ;
31+ import java .util .concurrent .ConcurrentHashMap ;
32+ import java .util .concurrent .ConcurrentMap ;
33+ import java .util .concurrent .atomic .LongAdder ;
2734
2835import org .eclipse .rdf4j .common .concurrent .locks .StampedLongAdderLockManager ;
2936import org .eclipse .rdf4j .sail .SailException ;
4148 */
4249class LmdbRecordIterator implements RecordIterator {
4350 private static final Logger log = LoggerFactory .getLogger (LmdbRecordIterator .class );
51+ private static final List <Character > COMPONENT_ORDER = List .of ('s' , 'p' , 'o' , 'c' );
52+ private static final ConcurrentMap <String , LongAdder > RECOMMENDED_INDEX_TRACKER = new ConcurrentHashMap <>();
4453 private final Pool pool ;
4554
4655 private final TripleIndex index ;
@@ -142,6 +151,14 @@ class LmdbRecordIterator implements RecordIterator {
142151 }
143152 }
144153
154+ static ConcurrentMap <String , LongAdder > getRecommendedIndexTracker () {
155+ return RECOMMENDED_INDEX_TRACKER ;
156+ }
157+
158+ static void resetIndexRecommendationTracker () {
159+ RECOMMENDED_INDEX_TRACKER .clear ();
160+ }
161+
145162 private static String computeIndexName (TripleIndex index , long subj , long pred , long obj , long context ) {
146163 String actual = new String (index .getFieldSeq ());
147164 int boundCount = countBound (subj , pred , obj , context );
@@ -154,12 +171,12 @@ private static String computeIndexName(TripleIndex index, long subj, long pred,
154171 return actual ;
155172 }
156173
157- String recommendation = buildRecommendedIndex ( subj , pred , obj , context );
158- if (recommendation == null || recommendation . equals ( actual ) ) {
174+ CandidateIndex recommendation = selectRecommendedIndex ( actual , subj , pred , obj , context );
175+ if (recommendation == null ) {
159176 return actual ;
160177 }
161178
162- return actual + " (scan; consider " + recommendation + ")" ;
179+ return actual + " (scan; consider " + recommendation . name + ")" ;
163180 }
164181
165182 private static int countBound (long subj , long pred , long obj , long context ) {
@@ -179,29 +196,264 @@ private static int countBound(long subj, long pred, long obj, long context) {
179196 return count ;
180197 }
181198
182- private static String buildRecommendedIndex (long subj , long pred , long obj , long context ) {
183- StringBuilder recommendation = new StringBuilder (4 );
184- appendIfBound (recommendation , 's' , subj >= 0 );
185- appendIfBound (recommendation , 'p' , pred >= 0 );
186- appendIfBound (recommendation , 'o' , obj >= 0 );
187- appendIfBound (recommendation , 'c' , context >= 0 );
188-
189- if (recommendation .length () == 0 ) {
199+ private static CandidateIndex selectRecommendedIndex (String actual , long subj , long pred , long obj ,
200+ long context ) {
201+ List <CandidateIndex > candidates = buildCandidateIndexes (subj , pred , obj , context );
202+ if (candidates .isEmpty ()) {
190203 return null ;
191204 }
192205
193- for (char component : new char [] { 's' , 'p' , 'o' , 'c' }) {
194- if (recommendation .indexOf (String .valueOf (component )) < 0 ) {
195- recommendation .append (component );
206+ CandidateIndex best = null ;
207+ for (CandidateIndex candidate : candidates ) {
208+ if (candidate .name .equals (actual )) {
209+ continue ;
210+ }
211+
212+ if (best == null || candidate .count > best .count
213+ || (candidate .count == best .count && candidate .patternScore > best .patternScore )
214+ || (candidate .count == best .count && candidate .patternScore == best .patternScore
215+ && candidate .orderDeviation < best .orderDeviation )
216+ || (candidate .count == best .count && candidate .patternScore == best .patternScore
217+ && candidate .orderDeviation == best .orderDeviation
218+ && candidate .name .compareTo (best .name ) < 0 )) {
219+ best = candidate ;
220+ }
221+ }
222+
223+ if (best == null ) {
224+ best = candidates .get (0 );
225+ }
226+
227+ for (CandidateIndex candidate : candidates ) {
228+ candidate .counter .increment ();
229+ }
230+
231+ return best ;
232+ }
233+
234+ private static List <CandidateIndex > buildCandidateIndexes (long subj , long pred , long obj , long context ) {
235+ List <Character > boundComponents = gatherBoundComponents (subj , pred , obj , context );
236+ if (boundComponents .isEmpty ()) {
237+ return Collections .emptyList ();
238+ }
239+
240+ List <Character > preferredOrder = determinePreferredOrder (subj , pred , obj , context , boundComponents );
241+
242+ List <String > boundPermutations = permuteBoundComponents (boundComponents );
243+ List <String > unboundPermutations = permuteUnboundComponents (boundComponents );
244+
245+ if (unboundPermutations .isEmpty ()) {
246+ unboundPermutations = Collections .singletonList ("" );
247+ }
248+
249+ List <CandidateIndex > result = new ArrayList <>(boundPermutations .size () * unboundPermutations .size ());
250+
251+ for (String bound : boundPermutations ) {
252+ for (String suffix : unboundPermutations ) {
253+ String candidate = bound + suffix ;
254+ addCandidate (result , candidate , preferredOrder , subj , pred , obj , context );
255+ }
256+ }
257+
258+ return result ;
259+ }
260+
261+ private static void addCandidate (Collection <CandidateIndex > candidates , String candidate ,
262+ List <Character > preferredOrder , long subj , long pred , long obj , long context ) {
263+ LongAdder counter = RECOMMENDED_INDEX_TRACKER .computeIfAbsent (candidate , key -> new LongAdder ());
264+ long count = counter .sum ();
265+ int score = computePatternScore (candidate , subj , pred , obj , context );
266+ int deviation = computeDeviation (candidate , preferredOrder );
267+ candidates .add (new CandidateIndex (candidate , count , score , deviation , counter ));
268+ }
269+
270+ private static List <Character > gatherBoundComponents (long subj , long pred , long obj , long context ) {
271+ List <Character > bound = new ArrayList <>(4 );
272+ if (subj >= 0 ) {
273+ bound .add ('s' );
274+ }
275+ if (pred >= 0 ) {
276+ bound .add ('p' );
277+ }
278+ if (obj >= 0 ) {
279+ bound .add ('o' );
280+ }
281+ if (context >= 0 ) {
282+ bound .add ('c' );
283+ }
284+ return bound ;
285+ }
286+
287+ private static List <Character > determinePreferredOrder (long subj , long pred , long obj , long context ,
288+ List <Character > boundComponents ) {
289+ List <Character > order = new ArrayList <>(COMPONENT_ORDER .size ());
290+
291+ if (subj >= 0 ) {
292+ order .add ('s' );
293+ }
294+
295+ boolean subjectBound = subj >= 0 ;
296+ boolean contextBound = context >= 0 ;
297+ boolean predicateBound = pred >= 0 ;
298+ boolean objectBound = obj >= 0 ;
299+
300+ if (contextBound && !subjectBound ) {
301+ if (objectBound ) {
302+ order .add ('o' );
303+ }
304+ if (predicateBound ) {
305+ order .add ('p' );
306+ }
307+ if (contextBound ) {
308+ order .add ('c' );
309+ }
310+ } else {
311+ if (predicateBound ) {
312+ order .add ('p' );
313+ }
314+ if (objectBound ) {
315+ order .add ('o' );
316+ }
317+ if (contextBound ) {
318+ order .add ('c' );
319+ }
320+ }
321+
322+ for (Character component : boundComponents ) {
323+ if (!order .contains (component )) {
324+ order .add (component );
325+ }
326+ }
327+
328+ for (Character component : COMPONENT_ORDER ) {
329+ if (!order .contains (component )) {
330+ order .add (component );
196331 }
197332 }
198333
199- return recommendation .toString ();
334+ return order ;
335+ }
336+
337+ private static List <String > permuteBoundComponents (List <Character > boundComponents ) {
338+ char [] items = toCharArray (boundComponents );
339+ boolean [] used = new boolean [items .length ];
340+ StringBuilder current = new StringBuilder (items .length );
341+ List <String > result = new ArrayList <>();
342+ permuteCharacters (items , used , current , result );
343+ return result ;
344+ }
345+
346+ private static List <String > permuteUnboundComponents (List <Character > boundComponents ) {
347+ List <Character > unbound = new ArrayList <>(COMPONENT_ORDER );
348+ for (Character component : boundComponents ) {
349+ unbound .remove (component );
350+ }
351+
352+ if (unbound .isEmpty ()) {
353+ return Collections .emptyList ();
354+ }
355+
356+ char [] items = toCharArray (unbound );
357+ boolean [] used = new boolean [items .length ];
358+ StringBuilder current = new StringBuilder (items .length );
359+ List <String > permutations = new ArrayList <>();
360+ permuteCharacters (items , used , current , permutations );
361+ return permutations ;
362+ }
363+
364+ private static void permuteCharacters (char [] items , boolean [] used , StringBuilder current ,
365+ List <String > result ) {
366+ if (current .length () == items .length ) {
367+ result .add (current .toString ());
368+ return ;
369+ }
370+
371+ for (int i = 0 ; i < items .length ; i ++) {
372+ if (!used [i ]) {
373+ used [i ] = true ;
374+ current .append (items [i ]);
375+ permuteCharacters (items , used , current , result );
376+ current .deleteCharAt (current .length () - 1 );
377+ used [i ] = false ;
378+ }
379+ }
380+ }
381+
382+ private static int computeDeviation (String sequence , List <Character > preferredOrder ) {
383+ int deviation = 0 ;
384+ for (int i = 0 ; i < sequence .length (); i ++) {
385+ char component = sequence .charAt (i );
386+ int preferred = preferredOrder .indexOf (component );
387+ if (preferred < 0 ) {
388+ preferred = preferredOrder .size ();
389+ }
390+ deviation += Math .abs (preferred - i );
391+ }
392+ return deviation ;
393+ }
394+
395+ private static int computePatternScore (String indexName , long subj , long pred , long obj , long context ) {
396+ int score = 0 ;
397+ for (int i = 0 ; i < indexName .length (); i ++) {
398+ char component = indexName .charAt (i );
399+ switch (component ) {
400+ case 's' :
401+ if (subj >= 0 ) {
402+ score ++;
403+ } else {
404+ return score ;
405+ }
406+ break ;
407+ case 'p' :
408+ if (pred >= 0 ) {
409+ score ++;
410+ } else {
411+ return score ;
412+ }
413+ break ;
414+ case 'o' :
415+ if (obj >= 0 ) {
416+ score ++;
417+ } else {
418+ return score ;
419+ }
420+ break ;
421+ case 'c' :
422+ if (context >= 0 ) {
423+ score ++;
424+ } else {
425+ return score ;
426+ }
427+ break ;
428+ default :
429+ throw new IllegalArgumentException ("invalid component '" + component + "' in index: "
430+ + indexName );
431+ }
432+ }
433+ return score ;
434+ }
435+
436+ private static char [] toCharArray (List <Character > components ) {
437+ char [] items = new char [components .size ()];
438+ for (int i = 0 ; i < components .size (); i ++) {
439+ items [i ] = components .get (i );
440+ }
441+ return items ;
200442 }
201443
202- private static void appendIfBound (StringBuilder builder , char component , boolean bound ) {
203- if (bound ) {
204- builder .append (component );
444+ private static final class CandidateIndex {
445+ final String name ;
446+ final long count ;
447+ final int patternScore ;
448+ final int orderDeviation ;
449+ final LongAdder counter ;
450+
451+ CandidateIndex (String name , long count , int patternScore , int orderDeviation , LongAdder counter ) {
452+ this .name = name ;
453+ this .count = count ;
454+ this .patternScore = patternScore ;
455+ this .orderDeviation = orderDeviation ;
456+ this .counter = counter ;
205457 }
206458 }
207459
0 commit comments