(This issue only applies once #2470914: Add Views support for individual fields has been committed (and only if it's committed in a form similar to the current proposal).)

As evidenced by several @todo comments, the SearchApiFieldTrait used by our Views field handlers has several problems regarding loaded entities:

  • Both nested entities and the top-level result items are loaded individually, not with multi-loads. Refactoring the code could help save quite a bit of performance there, I imagine.
  • Nested entities are currently loaded even if the required field values are set in the results, defeating part of its purpose. (The field values won't be overwritten, but all entities that would be needed to do so are still loaded.) To fix this, we should probably include information about which of the retrievedProperties actually requires a certain property/object in the return value of expandRequiredProperties(). (And then skip all those properties that are only required by properties already present on that row, of course. The complication here is "on that row", we can only determine which properties are present on a per-row basis.)
  • When loading the entity for a field (in most cases to add a link to that entity) we currently don't have a way to determine which of multiple entities corresponds to which of multiple field values. It currently works well in most cases, but for multi-valued fields (that aren't entity references) this would fail, linking the second value of the first entity to the second entity (or not linking it at all, if there is only one entity). This is probably an edge case, I can't imagine many people would want to link the items in a multi-valued field all to their parent entity, but it might happen, and it can easily be configured, so it should also work. Probably, the solution here is just, again, to remember more information while we create our data structures.

Once this works correctly, we should also add tests to make sure it stays that way.

Comments

drunken monkey created an issue. See original summary.

drunken monkey’s picture

Issue tags: +release target
drunken monkey’s picture

Grayside’s picture

I'm not sure if I read the code correctly, but it appears that even if attempting to avoid all entity loads, we are loading the entity to derive the URL for the "link to entity" option. I found this while looking for a way to present the canonical, absolute URL from indexing time as a field in the search results.

drunken monkey’s picture

Yes, since we can't know how an entity implements its link functionality, we have to load the whole entity for that case. If you want to avoid it, just disable "link to entity" and use "Rewrite results" instead (link + tokens).

Grayside’s picture

@#5 agreed. For the multi-site use case with Solr this becomes key: #2682347 since using tokens to do this will require access to the site value from the index to build the absolute URL.

For this use case in general, it would be great if the Views integration would respect the Solr index setting to "pull data from solr". This is a common need with advanced uses of Solr, and the lack of an easy way to enforce that in Views will create a lot of testing complexity.

drunken monkey’s picture

For this use case in general, it would be great if the Views integration would respect the Solr index setting to "pull data from solr". This is a common need with advanced uses of Solr, and the lack of an easy way to enforce that in Views will create a lot of testing complexity.

We can't really know about such backend-specific options, but in general, avoiding entity loads wherever possible is of course the goal of this issue.
A general "do not load items" option, e.g., in the query settings might also make sense, though, and could be a big help for such use cases, you're right. Would be a separate issue, though. (And I've not thought this through yet, so there might be smaller or larger obstacles to adding that.)
For a custom-code solution in the meantime, you could try overriding the datasource plugin with one for which you can manually disable/circumvent item loading temporarily. Just an idea, though.

daften’s picture

Priority: Major » Critical

I'm going to mark this as critical, since this nearly brought down our servers on launch. A test pointed to this as the basic problem and we fixed it with some dirty hacks. But if this isn't fixed, there'll be a lot of trouble for sites with views that have a lot of entities on them. In our case it was about 400 entities on a single view.

wim leers’s picture

Issue tags: +Performance, +scalability
drunken monkey’s picture

One problem the SearchApiFieldTrait::preRender() method definitely has is that it will call Index::getPropertyDefinitions() once for each result, each field and each encountered property while retrieving the field's values, which potentially multiplies to a staggering amount for a call to a non-trivial method, which each time will retrieve the properties from the entity type and all its bundles and then run them through the enabled processors that might change this. Didn't really spot that when implementing, but this method is actually not that cheap (even though all of the information retrieved is actually cached at a lower level already), and calling it this much can therefore alone cost quite a lot of time.

The (hopefully) simple solution here is, of course, to just cache those properties. There are three options for how/where to do that:

  1. Caching just within the method (so, still one call per field and datasource, just not per result row)
  2. Static cache within the index (just one uncached call per datasource)
  3. Static + persistent cache within the index

They are in order of increasing potential performance gain, but also increasing complexity and, thus, potential conflicts with other code. Attached are implementations for the first two options (adding a persistent cache should really just be a last ressort). It would be great if some of the people with performance problems could give those a try and try to benchmark the performance gained for both cases.
The patch for 2), if we decide for that, would still need a bit of fiddling to determine in which scenarios the static cache should really be cleared. However, this will probably only lead to actual problems very rarely (and, possibly, in the tests), so this one should be good enough for initial evaluation.

In any case, this is just a potential hotfix for anyone encountering problems. The method would still need an overhaul to tackle the more substantial underlying problems discussed in the IS.

@ daften: Just because it's critical for your site doesn't make the issue critical.

drunken monkey’s picture

Assigned: Unassigned » drunken monkey
Status: Active » Needs review

Forgot to set to "Needs review".
Also, I'm now tackling the larger issue of actually overhauling the preRender() method. Let's hope for the best …

Status: Needs review » Needs work
drunken monkey’s picture

Status: Needs work » Needs review
StatusFileSize
new2.07 KB
new3.63 KB

As suspected, running into a caching problems in the tests with the index-level caching. Easy to fix, though: the attached revision should at least prevent the highest-impact potential side effects. Though even getting into that situation is probably rather rare in actual use – it's a typical test thing, where too much happens in a single "page request"/process.

drunken monkey’s picture

StatusFileSize
new10.85 KB

And here is a much-needed test specifically for the preRender() method (not including any of the previous patches).
If we come up with an improved version of the method, we can also add assertions that no unnecessary entity loads occur. Those would fail now, of course, though.

(As you can see, this already uncovered a small problem in the current code.)

drunken monkey’s picture

StatusFileSize
new14.94 KB
new21.47 KB

This implements the second suggested improvement – avoiding to load properties that are already covered by what's on the result row object. It also fixes a problem I noticed with the $row->_relationship_objects array only being keyed by property path, not combined property path (e.g., body instead of entity:node/body), meaning that a datasource-independent property with the same name would overwrite those objects, leading to wrong data being displayed.
(Patch without the AggregatedFields doc comment fixes this time – sorry about that!)

(PS: The additional information in the remaining @todo comment is of course temporary, and only as a reminder for me. Please ignore.)

Status: Needs review » Needs work

The last submitted patch, 15: 2650986-15--overhaul_preRender.patch, failed testing. View results

drunken monkey’s picture

Status: Needs work » Needs review
StatusFileSize
new28.21 KB
new38.92 KB

This adds part of the first suggestion, using multi-loads wherever possible for nested properties.
To do this, I needed to split the giant foreach ($values as $row) loop into two. I used this opportunity to move both of the new loops into their own method, to make the code a bit cleaner (hopefully).

Multi-loading for result items still missing.

Status: Needs review » Needs work

The last submitted patch, 17: 2650986-17--overhaul_preRender.patch, failed testing. View results

drunken monkey’s picture

StatusFileSize
new9.63 KB
new43.02 KB

This broke viewing of Field API fields (with Field API rendering) and, thus, the tests.
This revision should fix that.

drunken monkey’s picture

Status: Needs work » Needs review
drunken monkey’s picture

StatusFileSize
new7.52 KB
new45 KB

This adds multi-loading for the result items. However, the effect is pretty much cancelled by \Drupal\search_api\Plugin\views\query\SearchApiQuery::addResults(), which has hard-coded single-loading of all result items, unfortunately. Since fixing that is out-of-scope for this issue, I've opened #2898327: Keep SearchApiQuery::addResults() from loading all result items individually as a follow-up.

borisson_’s picture

Status: Needs review » Needs work

This is a huge patch, so really hard to properly review, I took it method by method so I may have missed the bigger picture.
I've found 2 small things that could be improved. Looks like the kernel test covers everything that's needed.

  1. +++ b/src/Plugin/views/field/SearchApiFieldTrait.php
    @@ -597,6 +477,362 @@ protected function expandRequiredProperties() {
    +      // Same if the object was loaded on the result item already.
    +      $object = $row->_item->getOriginalObject(FALSE);
    +      if ($object) {
    +        $row->_object = $object;
    +        $row->_relationship_objects[NULL] = [$object];
    +      }
    

    Should we insert a continue; in here as well? To make sure the rest of the code is not executed.

  2. +++ b/src/Plugin/views/field/SearchApiFieldTrait.php
    @@ -597,6 +477,362 @@ protected function expandRequiredProperties() {
    +    if (!$to_load) {
    

    if (empty($to_load)) { is more explicit.

drunken monkey’s picture

Status: Needs work » Needs review
StatusFileSize
new616 bytes
new45.02 KB

Thanks a lot for reviewing!

This is a huge patch, so really hard to properly review

Yes, sorry about that. Maybe I should have saved the refactoring to helper methods for a second step?
Anyways, that's exactly why I wanted to have proper test coverage beforehand – with this in place, I'm pretty confident we're not breaking anything. However, there's of course still a lot of possibilities for errors – as your first remark demonstrates. Yes, there is of course a continue; missing, thanks a lot for noticing! This is probably a rather rare case, and will in most cases also not have massive consequences (the unnecessary load would probably be from static cache in most cases), but still, without the continue;, there's no point in that block at all.

Your second remark, though, is purely taste, and I personally dislike using empty() in cases where I know the variable (or whatever) is set. If there's a majority of people feeling that using empty() is better, I'd be open to still change it, but then we might want to go through the whole module changing this. I'm sure it's used in a lot of places already.

Would still be great if someone with actual performance problems with a search view could give these patches (here and #10/#13) a whirl and tell us how they affect performance for them. I'm pretty certain all of them will improve performance, but it's hard to say by how much.

borisson_’s picture

Issue tags: -Needs tests

Your second remark, though, is purely taste, and I personally dislike using empty() in cases where I know the variable (or whatever) is set. If there's a majority of people feeling that using empty() is better, I'd be open to still change it, but then we might want to go through the whole module changing this. I'm sure it's used in a lot of places already.

You're right, this is purely taste, but I generally prefer not to depend on implicit type-juggling. (in this case, an array being cast to a boolean, and because it's empty it resolves to false.), Even more explicit is using count($to_load) === 0, because that's what eventually ends up happening.

It's just a preference though, so no need to change that.

The refactoring might've been easier in a second step, or maybe refactor first, changes later. Doesn't matter now anyway - untangling this would be a lot of work.

I agree that an actual test on a big dataset would be a great thing. I've removed the needs tests tag, as we have sufficient coverage I think.

drunken monkey’s picture

StatusFileSize
new2.69 KB
new46.64 KB
borisson_’s picture

Status: Needs review » Reviewed & tested by the community

This looks good. Haven't tested it, just looked over the code again.

drunken monkey’s picture

StatusFileSize
new50.26 KB

Thanks a lot for reviewing!
Would have been great to get actual tests (especially from the people who paid good money for this), but apparently it's not happening and I'm tired of waiting. Even if this ends up not doing anything for performance, it still fixes some actual problems and improves test coverage.

The attached patch combines #13 and #25. If the test bot agrees, I'll commit it.

  • drunken monkey committed 1eface9 on 8.x-1.x
    Issue #2650986 by drunken monkey, borisson_: Fixed various problems with...
drunken monkey’s picture

Status: Reviewed & tested by the community » Fixed

Committed.
Thanks again, everyone!

drunken monkey’s picture

OK, cheered too early, this lead to an uncaught exception in some cases: #2910918: Form for configurable fields is broken (Edit: Corrected link – thanks, Joris!).

borisson_’s picture

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.