Problem/Motivation

When using search_api_db as the backend, under some conditions the pertinence score for a keyword search is not calculated correctly, resulting in erroneous ordering of search results per pertinence.

It happens when the search index is configured with multiple (full) text fields, with multiple fields containing the same boost factor, and when making a search query that matches a word contained in multiple fields with the same boost factor.

Steps to reproduce

  • Have a content type with 2 fields: "body", and "additional", with the 2 fields being displayed on the node page
  • Configure the search index so that the fields "body", "additional" and "rendered item" are indexed as Full text
  • Assign a boost factor of 1 to "body" and "rendered item", and a boost factor of 0.1 to "additional"
  • Enable the Tokenizer processor
  • Create 2 nodes of the above content type: Node 1 containing "keyword" in the body field, and Node 2 containg "keyword" in the additional field
  • Create a search view using the search index, that performs a full text search across all fields and orders results by pertinence
  • Perform a search for "keyword"
  • Instead of seeing "Node 1", then "Node 2" in the results, "Node 2" is ranked first

It happens because Drupal\search_api_db\Plugin\search_api\backend::createKeysQuery() builds a query with a "GROUP BY item_id, score". If multiple rows have the same score for the same item, only one of them will be retained when calculating the final pertinence score.

In this case, Node 1 will have 2 rows with the same score for "keyword" (each of them = 5000), and Node 2 will have 2 rows with different scores (5000 and 500), due to the boost factor. Due to the buggy GROUP BY, Node 1 ends up with a score of 5000 (instead of 10'000), and Node 2 ends up with a score of 5500.

Proposed resolution

Replacing the "GROUP BY score" (which makes no sense) with a "GROUP BY field_name", which will ensure that 2 fields with the same boost factor containing the same word will be calculated correctly.

Issue fork search_api-3225675

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

mdupont created an issue. See original summary.

mdupont’s picture

Status: Active » Needs review
drunken monkey’s picture

Version: 8.x-1.20 » 8.x-1.x-dev
StatusFileSize
new1.84 KB
new3.19 KB

Thanks a lot for reporting this problem and attempting a fix.

However, please use patches in this project instead of issue forks, as the d.o team has been unable/unwilling to get issue fork testing working for it yet. (See #3190024: Problem with test dependencies when testing issue forks.)

Running the tests manually for your patch, however, shows that it leads to incorrect SQL queries in some scenarios. However, I think I was able to determine the correct fix – the problem was that we didn’t sum the score in all the right scenarios. The code was never meant to group by score in any scenario. I now added a comment to make that clear.

However, I’m unfortunately still unable to reproduce this problem. As you see, I tried to write a regression test for it, but couldn’t get it to fail with the unpatched version. Are you maybe able to get it to work? That would be the last thing to do before I can commit it, I think.
(What I can see pretty clearly from the code, I think, is that the bug will only trigger if the match mode is not set to 'words', i.e., partial matching is enabled.)

Of course, please also test/review the patch so far and see if it also fixes the problem.
Thanks again!

drunken monkey’s picture

Status: Needs review » Needs work

NW for the missing/incomplete regression test.

mdupont’s picture

Thanks for your useful feedback. I'm trying to come up with a regression test that triggers this issue.

In the meantime, I confirm I was able to reproduce the issue on a stock Drupal install with Search API 8.x-1.x-dev (latest git commit), with a simple setup:

  1. Enable "Database search" module
  2. Create a search server for DB, in the settings enable partial matching
  3. Create an index on this search server, indexing "Content"
  4. Add "Title" and "Body" fields as "Full text" with the same boost factor of 1.0
  5. Create a view on the search index, with a full-text search contextual filter on all indexed fields, displaying the node title and the pertinence score
  6. Create 2 nodes: node 1 with "testkeyword" in the title only, node 2 with "testkeyword" both in the title and the body
  7. Use the view to search for "testkeyword"
  8. Expected: display node 2 with a score of 2000, and node 1 with a score of 1000. Actual: both nodes have a score of 1000.

Using Webprofiler, we see that the view performs the following SQL query:
SELECT "t"."item_id" AS "item_id", SUM(t.score) AS "score" FROM (SELECT "t"."item_id" AS "item_id", "t"."score" AS "score", CASE WHEN t.word LIKE '%testkeyword%' THEN 1 ELSE 0 END AS "w1" FROM "search_api_db_ind_text" "t" WHERE ("t"."word" LIKE '%testkeyword%' ESCAPE '\') AND ("t"."field_name" IN ('body', 'title')) GROUP BY t.item_id, t.score, w1) "t" GROUP BY t.item_id ORDER BY "score" DESC LIMIT 11 OFFSET 0

We can check that running this query in the DB results in both nodes having the same score.

Now, if we alter the SQL query just a bit to add a GROUP BY t.field_name as such:
SELECT "t"."item_id" AS "item_id", SUM(t.score) AS "score" FROM (SELECT "t"."item_id" AS "item_id", "t"."score" AS "score", CASE WHEN t.word LIKE '%testkeyword%' THEN 1 ELSE 0 END AS "w1" FROM "search_api_db_ind_text" "t" WHERE ("t"."word" LIKE '%testkeyword%' ESCAPE '\') AND ("t"."field_name" IN ('body', 'title')) GROUP BY t.item_id, t.field_name, t.score, w1) "t" GROUP BY t.item_id ORDER BY "score" DESC LIMIT 11 OFFSET 0

This time when we run the query we see node 2 having a score of 2000 and node 1 having a score of 1000, as expected.

Back to trying to correctly write a regression test for this.

mdupont’s picture

Testing whether an updated regression test does trigger the regression.

The patch in #4 was not working because items were not marked for reindexing after changing the boost factor for the "name" field, so the call to $this->indexItems() was doing nothing and the query results were incorrect.

mdupont’s picture

This patch for a regression test should work. The generated query in previous patches was incorrectly sorting results.

mdupont’s picture

Status: Needs work » Needs review
StatusFileSize
new3.46 KB
new2.12 KB

Test only patch should fail, the other one should pass.

Test failures on D9.3 are due to the state of Drupal core 9.3.x-dev and not due to this patch.

Status: Needs review » Needs work
mdupont’s picture

Status: Needs work » Needs review

Back to NR as test failures were expected.

The last submitted patch, 10: 3225675-10--db_backend_score_aggregation.patch, failed testing. View results

mdupont’s picture

Bundled a fix for the failing tests on Drupal 9.3, even if distinct from this issue.

Status: Needs review » Needs work

The last submitted patch, 14: 3225675-12--db_backend_score_aggregation.patch, failed testing. View results

mdupont’s picture

Status: Needs work » Needs review
StatusFileSize
new4.8 KB

Forgot to fix one call to Role::create().

mdupont’s picture

In addition to passing the tests, I confirm the patch in #16 also fixes the issue on a real Drupal website performing searches via Views.

  • drunken monkey committed 5e8a82a on 8.x-1.x authored by mdupont
    Issue #3225675 by mdupont, drunken monkey: Fixed score calculation in DB...
drunken monkey’s picture

Status: Needs review » Fixed

Thanks a lot, great work! And sorry for taking so long to respond.
We fixed the test problems with Drupal 9.3 already, so I used your patches from #10.
I just made some slight changes and then committed. Thanks again!

Status: Fixed » Closed (fixed)

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